package sub

import (
	"bufio"
	"context"

	// "crypto/internal/edwards25519/field"
	"fmt"
	"log"
	"net"
	"os"
	"os/exec"
	"path/filepath"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/reflection"
	"google.golang.org/grpc/status"

	// "github.com/pkg/errors"

	"strconv"
	"strings"
	"time"

	"github.com/fsnotify/fsnotify"

	pb "a.local/generated_pb" // import ./util as u
	// 🦜 : for DiscoverGateway()
)

var (
	myIp     string
	dryRun   bool
	safety   int
	mockMore bool
	cwd      string
)

type server struct {
	pb.UnimplementedGreeterServer
}

func figuraOurPath() (string, string, error){
	// figure out what path should we watch

	// 0. figure out whose currently using the computer
	cmd := exec.Command("last", "--time-format", "iso")
	output, err := cmd.Output()
	if err != nil {
		// return /tmp
		msg := fmt.Sprintf("获取用户登录历史错误: %v", err)
		return "","", status.Error(codes.Internal, msg)
	}

	// 0.1 parse the first entry
	lines := strings.Split(string(output), "\n")
	u, err := parseLineIntoLoginInfo(lines[0])
	if err != nil {
		msg := fmt.Sprintf("Parsing用户登录历史错误: %v", err)
		log.Printf("🦜 : Error parsing line %v:", lines[0])
		return "" ,"", status.Error(codes.Internal, msg)
	}

	// 0.2 just get the user
	user := u.User
	log.Printf("🦜 : The current user is found to be %s", user)

	// 1. If /<user>/Desktop or /home/<user>/Desktop/ exists, that's what we're looking for
	desktop := filepath.Join("/", user, "Desktop")
	_, err = os.Stat(desktop)
	if err == nil {
		return desktop,user, nil
	}
	desktop = filepath.Join("/home", user, "Desktop")
	_, err = os.Stat(desktop)
	if err == nil {
		return desktop, user, nil
	}

	// 2. Else if /home/<user>/ or /<user>/ exists, that's what we're looking for
	home := filepath.Join("/home", user)
	_, err = os.Stat(home)
	if err == nil {
		return home, user, nil
	}
	home = filepath.Join("/", user)
	if err == nil {
		return home, user, nil
	}

	// 3. Else return /tmp
	return "/tmp", user, nil
}

type FileIORecord struct {
	UnixMilli int64  `json:"time"`
	EventType string `json:"event"`
	FileName  string `json:"file"`
}

func listenToIO(out chan<- FileIORecord, stopButton <-chan struct{}, path string) {
	// e.g. path = /tmp

	// 0. Start the watcher
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	// Start listening for events.
	go func() {
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					log.Println("❌️ Error reading events, watcher closed")
					return
				}

				if event.Has(fsnotify.Chmod) {
					// skip this event
					continue
				}

				// log.Println("🦜 event:", event)
				out <- FileIORecord{
					UnixMilli: time.Now().UnixMilli(),
					EventType: event.Op.String(),
					FileName:  event.Name,
				}
				// if event.Has(fsnotify.Write) {
				// 	log.Println("modified file:", event.Name)
				// }

			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Println("❌️ error:", err)
			}
		}
	}()

	// Add the path.
	err = watcher.Add(path)
	if err != nil {
		log.Fatal(err)
	}
	// Block until stopButton is pressed.
	<-stopButton
}

func (s *server) StreamFileIO(in *pb.HelloRequest, stream pb.Greeter_StreamFileIOServer) error {
	ear := make(chan FileIORecord)
	stopButton := make(chan struct{})
	var p string
	var user string

	if in.Name == "" {
		// 0. figure out the path
		p1, u0, err := figuraOurPath()
		if err != nil {
			msg := fmt.Sprintf("Error figuring out the path: %v", err)
			return status.Error(codes.Internal, msg)
		}
		user = u0
		p = p1
	} else {
		p = in.Name
	}

	log.Printf("🦜 : Listening to file path %s", p)
	go listenToIO(ear, stopButton, p)
	for {
		oi := <-ear
		r := &pb.FileIORecord{
			UnixMilli: oi.UnixMilli,
			EventType: oi.EventType,
			FileName:  oi.FileName,
			User:     user,
		}
		if err := stream.Send(r); err != nil {
			log.Printf("Error sending to the stream: %v", err)
			break
		}
	}
	stopButton <- struct{}{}
	return nil
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Msg: "Hello, " + in.GetName()}, nil
}

func parseLineIntoUserInfo(line string) (*pb.UserInfo, error) {
	fields := strings.Split(line, ":")
	if len(fields) != 7 {
		return nil, fmt.Errorf("line format error: %s", line)
	}

	uid := fields[2]
	name := fields[0]
	homeDir := fields[5]
	description := fields[4]
	shell := fields[6]

	return &pb.UserInfo{
		Uid: uid, Name: name,
		HomeDir:     homeDir,
		Description: description,
		Shell:       shell,
	}, nil
}

func GetUsers0() (*pb.GetUsersRpl, error) {
	/*
	   🦜 : We took this out because we kinda need this in getBashHistory()
	*/
	cmd := exec.Command("getent", "passwd")
	output, err := cmd.Output()
	if err != nil {
		msg := fmt.Sprintf("获取用户列表错误: %v", err)
		return nil, status.Error(codes.Internal, msg)
	}

	lines := strings.Split(string(output), "\n")
	r := pb.GetUsersRpl{}
	for _, line := range lines {
		if len(line) == 0 {
			continue
		}
		u, err := parseLineIntoUserInfo(line)
		if err != nil {
			continue
		}
		r.Users = append(r.Users, u)
	}
	return &r, nil
}

func (s *server) GetUsers(ctx context.Context, in *pb.Empty) (*pb.GetUsersRpl, error) {
	// 🦜 : Feels harmless, so not checking

	return GetUsers0()
}

func parseLineIntoLoginInfo(l string) (*pb.LoginInfo, error) {
	/*
	   🦜 : Example lines:

	   0        1            2                3                         4 5                           6
	   me       tty2         tty2             2023-12-25T10:11:28+08:00 - down                       (09:08)
	   reboot   system boot  6.2.0-39-generic 2023-12-25T10:11:07+08:00 - 2023-12-25T19:19:44+08:00  (09:08)

	*/
	if len(l) == 0 {
		return nil, fmt.Errorf("empty line")
	}

	f := strings.Fields(l)
	// if it starts with `reboot`,`system`,`boot`, concat fields 1,2 (0-based)
	if f[0] == "reboot" && f[1] == "system" && f[2] == "boot" {
		f[2] = "system boot"
		f[1] = f[0]
		f = f[1:]
	}

	if len(f) != 7 {
		return nil, fmt.Errorf("line format error, expect 7 fields: %s", l)
	}

	// parse them
	user := f[0]
	terminal := f[1]
	loginIp := f[2]

	// theirLayout := "2006-01-02T15:04:05-07:00"
	/*
	   🐢 : In go, both "-07:00" and "Z07:00" can parse the time zone
	   "+08:00", they are synonym.
	*/

	loginTime, err := time.Parse(time.RFC3339, f[3])
	if err != nil {
		return nil, err
	}

	humanLayout := "2006-01-02 15:04:05"
	loginTimeStr := loginTime.Format(humanLayout)

	// field 5 can have special values: `down`, `crash`
	var logoutTimeStr string
	// 🦜 : Let's first try parse it as a time, if it fails, then we just
	// store the whole string...(U know what? let's store all the strings to
	// till the end of f)
	t, err := time.Parse(time.RFC3339, f[5])
	if err != nil {
		// store f[5:]
		logoutTimeStr = strings.Join(f[5:], " ")
	}else{
		logoutTimeStr = t.Format(humanLayout)
	}

	return &pb.LoginInfo{
		User:       user,
		Terminal:   terminal,
		LoginIp:    loginIp,
		LoginTime:  loginTimeStr,
		LogoutTime: logoutTimeStr,
	}, nil

}

func (s *server) GetLoginHistory(ctx context.Context, in *pb.GetLoginHistoryReq) (*pb.GetLoginHistoryRpl, error) {

	// 🦜 : Feels harmless, so not checking
	// if dryRun {
	// 	/*
	// 	   message LoginInfo {
	// 		string user = 1;
	// 		string terminal = 2;
	// 		string loginIp = 3;
	// 		string loginTime = 4;
	// 		string logoutTime = 5;
	//            }

	// 	   message GetLoginHistoryRpl {
	// 	            repeated LoginInfo loginInfos = 1;
	// 	   }
	// 	 */
	// 	return &pb.GetLoginHistoryRpl{
	// 		LoginInfos: []*pb.LoginInfo{
	// 			{User: "me", Terminal: "tty2", LoginIp: "tty2", LoginTime: "2021-01-01 12:00:00", LogoutTime: "2021-01-01 22:00:00",},
	// 			{User: "reboot", Terminal: "system boot", LoginIp: "6.2.0-39-generic", LoginTime: "2021-01-01 12:00:00", LogoutTime: "2021-01-01 22:00:00",},
	// 			{User: "me", Terminal: "tty2", LoginIp: "tty2", LoginTime: "2022-01-01 12:00:00", LogoutTime: "2022-01-01 22:00:00",},
	// 			{User: "me", Terminal: "tty2", LoginIp: "tty2", LoginTime: "2023-01-01 12:00:00", LogoutTime: "2023-01-01 22:00:00",},
	// 		},
	// 	}, nil
	// }

	/*
		   🐢 : Do something like:

		last --since '2023-12-25 09:00:00' --until '2023-12-28 09:00:00' --time-format iso <name>

		   so passed in date should already be acceptable by `last`
	*/

	var cmd *exec.Cmd
	if in.GetUser() == "" {
		cmd = exec.Command("last", "--since", in.GetSinceTime(), "--until", in.GetUntilTime(), "--time-format", "iso")
	} else {
		cmd = exec.Command("last", "--since", in.GetSinceTime(), "--until", in.GetUntilTime(), "--time-format", "iso", in.GetUser())
	}
	output, err := cmd.Output()
	if err != nil {
		msg := fmt.Sprintf("获取用户登录历史错误: %v", err)
		return nil, status.Error(codes.Internal, msg)
	}

	lines := strings.Split(string(output), "\n")
	r := pb.GetLoginHistoryRpl{}
	for _, line := range lines {
		u, err := parseLineIntoLoginInfo(line)
		if err != nil {
			continue
		}
		r.LoginInfos = append(r.LoginInfos, u)
	}

	return &r, nil
}

func tryAddHisOrHerEntries(u *pb.UserInfo, r *pb.GetBashHistoryRpl) (*pb.GetBashHistoryRpl, error) {
	// 1. if there's a .bash_history in their HOME, get it. If there's none, just return
	historyFile := filepath.Join(u.HomeDir, ".bash_history")
	_, err := os.Stat(historyFile)
	if err != nil { // file does not exist
		return r, nil
	}

	/*
		   🦜 : How to parse this?

		   🐢 : The file consists of many "entries". An entry can be either one
		   line or two lines.

		   One-line entry contains only the command, like:
		                sudo --help

		   Two-line entry contains first the #timestamp and then the command, like:
				#1704962500
				sudo -u keystone bash -c 'echo hi'

		   🦜 : So we can just keep parsing until we see the end of the file, right?

		   🐢 : Yeah, so index-based for loop is good enough.
	*/

	// 2. get the file
	cmd := exec.Command("bash", "-c", fmt.Sprintf("tail -n %d %s", 500, historyFile)) // 🦜 : 500 lines max
	output, _ := cmd.Output()

	// 将输出转换为字符串并分割行
	lines := strings.Split(string(output), "\n")

	// 3. parse the file
	for i := 0; i < len(lines)-1; {
		if strings.HasPrefix(lines[i], "#") && i+1 < len(lines) {
			/*
			   message BashHistoryEntry {string cmd = 1; string time = 2; string user = 3;}
			*/

			e := &pb.BashHistoryEntry{}
			e.User = u.Name

			// try parse a two-line entry
			s := strings.TrimLeft(lines[i], "#")
			timestamp, err := strconv.ParseInt(s, 10, 64)
			if err != nil {

				// msg := fmt.Sprintf("Unexpected format of .bash_history while parsing timestamp : %v", err)
				// return nil, status.Error(codes.Internal, msg)

				// --------------------------------------------------
				/*
				 <2024-09-06 五> 🦜 : Actually, we can just be a
				 little bit robust and set the Time to
				 "unknown", this is just a comment line...
				*/
				i += 1
			} else {
				// this is a valid timestamp, so parse the next line
				e.Time = time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
				e.Cmd = lines[i+1]
				// add to the response
				r.Entries = append(r.Entries, e)
				// continue to parse the next entry
				i += 2
			}
		} else { // parse a one-line entry
			e := &pb.BashHistoryEntry{}
			e.Cmd = lines[i]
			e.Time = "unknown"
			e.User = u.Name
			r.Entries = append(r.Entries, e)
			i += 1
		}
	}
	return r, nil
}

/*
🐢 : In order for the bash to record the time of each command, do

echo 'HISTTIMEFORMAT="%F %T "' >> /etc/environment
*/
func GetBashHistory0() (*pb.GetBashHistoryRpl, error) {
	// 1. get all the users
	users, err := GetUsers0()
	if err != nil {
		return nil, err
	}

	// 2. try add his or her entries
	r := &pb.GetBashHistoryRpl{}
	for _, u := range users.Users {

		r, err = tryAddHisOrHerEntries(u, r)
		if err != nil {
			return nil, err
		}
	}

	// 3. return
	return r, nil
}

func (s *server) GetBashHistory(ctx context.Context, in *pb.Empty) (*pb.GetBashHistoryRpl, error) {
	// 🦜 : Needs root to read other users' .bash_history, so we need to mock it
	if dryRun {
		return &pb.GetBashHistoryRpl{
			Entries: []*pb.BashHistoryEntry{
				{Cmd: "sudo --help", Time: "2021-01-01 12:00:00", User: "me"},
				{Cmd: "sudo -u keystone bash -c 'echo hi'", Time: "2021-01-01 12:00:00", User: "me"},
				{Cmd: "go test .", Time: "unknown", User: "me"},
				{Cmd: "ls /etc/environment", Time: "2021-01-01 12:00:00", User: "me"},
				{Cmd: "less ~/.bash_history", Time: "2021-01-01 12:00:00", User: "me"},
				{Cmd: "sudo --help", Time: "2021-01-01 12:00:00", User: "aaa"},
				{Time: "2024-01-15 19:05", Cmd: "git commit -m 'Start adding bash history'", User: "me"},
				{Time: "2024-01-15 19:06", Cmd: "gpsh main", User: "me"},
				{Time: "2024-01-16 10:30", Cmd: "got", User: "me"},
				{Time: "2024-01-16 10:30", Cmd: "l", User: "me"},
				{Time: "2024-01-16 10:30", Cmd: "cd go-agent", User: "me"},
				{Time: "2024-01-16 10:30", Cmd: "ls", User: "me"},
				{Time: "2024-01-16 10:31", Cmd: "cat hi.zsh", User: "me"},
				{Time: "2024-01-16 10:31", Cmd: "go run . --safety=2 --mockMore=true\n", User: "me"},
				{Time: "2024-01-16 10:31", Cmd: "ls", User: "me"},
				{Time: "2024-01-16 10:31", Cmd: "emacs src/main/java/aaa/M.java &", User: "me"},
				{Time: "2024-01-16 10:32", Cmd: "idea .", User: "me"},
				{Time: "2024-01-16 10:35", Cmd: "cd ..", User: "me"},
				{Time: "2024-01-16 10:35", Cmd: "./mvnw test", User: "me"},
				{Time: "2024-01-16 10:37", Cmd: "l", User: "me"},
				{Time: "2024-01-16 10:37", Cmd: ". ./s.sh", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "history ", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "history --help", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "history -h", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "history -t", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "history help", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "clear", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "help history", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "history -h", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "ddg zsh history", User: "me"},
				{Time: "2024-01-16 10:39", Cmd: "cfw", User: "me"},
				{Time: "2024-01-16 10:40", Cmd: "fc", User: "me"},
				{Time: "2024-01-16 10:40", Cmd: "cfw", User: "me"},
				{Time: "2024-01-16 10:41", Cmd: "fc -l", User: "me"},
				{Time: "2024-01-16 10:41", Cmd: "fc --help", User: "me"},
				{Time: "2024-01-16 10:41", Cmd: "hc -h", User: "me"},
				{Time: "2024-01-16 10:41", Cmd: "man history", User: "me"},
				{Time: "2024-01-16 10:41", Cmd: "clear", User: "me"},
				{Time: "2024-01-16 11:01", Cmd: "history", User: "me"},
				{Time: "2024-01-16 11:02", Cmd: "history", User: "me"},
				{Time: "2024-01-16 11:02", Cmd: "cat ~/.zsh_aliases", User: "me"},
				{Time: "2024-01-16 11:02", Cmd: "cat ~/.zsh_history", User: "me"},
				{Time: "2024-01-16 11:02", Cmd: "clear", User: "me"},
				{Time: "2024-01-16 11:02", Cmd: "history", User: "me"},
				{Time: "2024-01-16 11:03", Cmd: "history -n 5", User: "me"},
				{Time: "2024-01-16 11:03", Cmd: "ddg zsh history timestamp", User: "me"},
				{Time: "2024-01-16 11:03", Cmd: "history -E", User: "me"},
				{Time: "2024-01-16 11:03", Cmd: "history -i", User: "me"},
				{Time: "2024-01-16 11:03", Cmd: "clear", User: "me"},
				{Time: "2024-01-16 11:03", Cmd: "history -E", User: "me"},
				{Time: "2024-01-16 11:04", Cmd: "clear", User: "me"},
			},
		}, nil
	}

	return GetBashHistory0()
}

func getBootTime0() string {
	// 服务器开机时间 uptime -s
	var execBootTimeCmd = "uptime -s"
	bootTimeCmd := exec.Command("bash", "-c", execBootTimeCmd)
	bootTimeOutput, _ := bootTimeCmd.Output()

	// 解析输出，获取当前正在运行的应用
	BootTimeLines := strings.Split(string(bootTimeOutput), "\n")
	for _, v := range BootTimeLines {
		if len(v) == 0 {
			continue
		}
		return v
	}
	return ""
}

func (s *server) GetBootTime(ctx context.Context, in *pb.Empty) (*pb.HelloReply, error) {
	r := &pb.HelloReply{}
	r.Msg = getBootTime0()
	return r, nil
}

func (s *server) StreamOneoffProcessInfo(in *pb.Empty, stream pb.Greeter_StreamOneoffProcessInfoServer) error {
	// 1. make output channel for the OneoffProcessInfo
	// --------------------------------------------------
	/*
	 */

	if dryRun {
		cms := [3]string{
			"ls-mocked -l /home/me/Music/black-footed-cat-purr.mp3",
			"cd-mocked /home/me/Music",
			"echo-mocked hi",
		}

		/*
		   🦜 : For compatibility reason, in this branch we just mock some data for every two minutes
		*/
		var i uint32
		i = 0
		for {
			i = i % 3
			argv := cms[i]
			r := &pb.OneoffProcessInfo{
				UnixMilli: time.Now().UnixMilli(),
				Comm:      strings.Split(argv, " ")[0],
				Pid:       i,
				Ppid:      i + 1,
				Uid:       i + 2,
				Argv:      argv,
				RetVal:    0,
			}
			if err := stream.Send(r); err != nil {
				log.Printf("Error sending to the stream: %v", err)
				break
			}
			i += 1
			// time.Sleep(30 * time.Second)
			time.Sleep(3 * time.Second)
		}
	} else {
		ear := make(chan OneoffProcessInfo)
		stopButton := make(chan struct{})
		go ListenToOneOff(ear, stopButton)
		// the consumer 🦜: pass to the stream
		for {
			oi := <-ear
			// fmt.Printf("🦜 : Learned %v\n", oi)
			// 🦜 ^ for debugging purpose..
			r := &pb.OneoffProcessInfo{
				UnixMilli: oi.UnixMilli,
				Comm:      oi.Comm,
				Pid:       oi.Pid,
				Ppid:      oi.Ppid,
				Uid:       oi.Uid,
				Argv:      oi.Argv,
				RetVal:    oi.RetVal,
			}
			if err := stream.Send(r); err != nil {
				log.Printf("Error sending to the stream: %v", err)
				break
			}
		}
		stopButton <- struct{}{}
	}

	log.Println("👋 : StreamOneoffProcessInfo done.")
	return nil
}

func Start(mmyIp string, port int, ssafety int, mmockMore bool) {
	myIp = mmyIp
	safety = ssafety
	dryRun = (safety == 2)
	mockMore = mmockMore

	cwd, err := os.Getwd()
	log.Println("⚙️ :cwd: " + Green(cwd))
	if err != nil { // unlikely
		msg := fmt.Sprintf("获取当前目录错误: %v", err)
		log.Fatal(msg)
	}

	// --------------------------------------------------
	// 1. Listen on port

	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	reflection.Register(s)
	log.Printf("server listening at %v", lis.Addr())

	// --------------------------------------------------
	// 2. Start listening to the

	// --------------------------------------------------
	// 2. Start the server in a goroutine
	go func() {
		if err := s.Serve(lis); err != nil {
			log.Fatalf("failed to serve: %v", err)
		}
	}()
	defer stop(s) // 🐢 if any thing bad (kinda finally)

	// --------------------------------------------------
	// 3. wait for an input
	fmt.Println("Press 'Enter' to exit...")
	scn := bufio.NewScanner(os.Stdin)
	scn.Scan()
	log.Println("👋 bye.")
	// 4. 🦜 : stop() should be called here.
}

func stop(s *grpc.Server) {
	// 🐢 : Graceful stop
	s.GracefulStop()
	log.Println("\t 🚮️ Server gracefully shut down.")
}
